# Enemy class

import random
import math

from shared.events import NORTH, EAST, WEST, SOUTH, ChangeDirection
from shared.entities import Entity


def reverse_direction(direction):
    return (
        -direction[0],
        -direction[1]
    )


def reconstruct_path(came_from, current):
    total_path = [current]
    while current in came_from:
        current = came_from[current]
        total_path.append(current)
    return total_path


class Enemy(Entity):
    def __init__(self, location, host, version):
        super(Enemy, self).__init__(location, host)

        self.version = version
        self.speed = 12

        self._ai_delay = self.speed  # Glitches may occur if not set to speed.
        self._current_tick = 0
        self.open_squares = None

    def get_open_squares(self, location=None):

        # default value for location is current location
        if location is None:
            location = self.location

        open_squares = {
            NORTH: False,
            SOUTH: False,
            WEST: False,
            EAST: False
        }

        for key in open_squares:
            if self.host.maze.is_coordinate(location[0], location[1]):
                open_squares[key] = self.host.maze[
                    location[0] + key[0],
                    location[1] + key[1]
                ]

        return open_squares

    def move(self):
        self._current_tick += 1
        if self._current_tick >= self._ai_delay:
            self._current_tick = 0

            # find open squares adjacent to enemy
            self.open_squares = self.get_open_squares()
            if self.version == 1:
                return self.move1()
            elif self.version == 2:
                return self.move2()
            elif self.version == 3:
                return self.move3()

    def move1(self):
        # move1 is almost random, but never reverses if possible.

        available_squares = []
        for key in self.open_squares:
            if self.open_squares[key]:
                available_squares.append(key)

        if len(available_squares) == 1:
            return ChangeDirection(self, available_squares[0])
        elif self._facing is not None:
            # Don't allow reversing.
            rev = reverse_direction(self._facing)
            if rev in available_squares:
                available_squares.remove(rev)

        direction = random.choice(available_squares)
        return ChangeDirection(entity=self, direction=direction)

    def move2(self):
        # move2 is the 'chaser' mode
        path = self.find_path_astar()
        return ChangeDirection(entity=self, direction=path['direction'])

    def find_path_astar(self):
        start = self.location
        end = self.host.player.location

        closedset = set()  # The set of nodes already evaluated.
        openset = {start}  # The set of tentative nodes to be evaluated, initially containing the start node
        came_from = {}  # The map of navigated nodes.
        g_score = {}
        f_score = {}

        g_score[start] = 0  # Cost from start along best known path.
        # Estimated total cost from start to goal through y.
        f_score[start] = g_score[start] + math.hypot(start[0] - end[0], start[1] - end[1])

        while openset:
            current = min(openset, key=lambda x: f_score[x])
            if current == end:
                rp = reconstruct_path(came_from, end)
                return {
                    'distance': len(rp),
                    'direction': (
                        rp[-2][0] - start[0],
                        rp[-2][1] - start[1]
                    )
                }

            openset.remove(current)
            closedset.add(current)
            for neighbor in self.host.maze.get_neighbours(current[0], current[1]):
                if neighbor in closedset:
                    continue
                tentative_g_score = g_score[current] + 1

                if neighbor not in openset or tentative_g_score < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    f_score[neighbor] = g_score[neighbor] + math.hypot(neighbor[0] - end[0], neighbor[1] - end[1])
                    if neighbor not in openset:
                        openset.add(neighbor)

    def move3(self):
        # Checks if distance is less than a specified number (Can vary based on difficulty, default is 15)
        # If less than specified number than activates 'chaser' mode
        # If more than specified number than activates 'random' mode
        if math.hypot(self.location[0] - self.host.player.location[0],
                      self.location[1] - self.host.player.location[1]) < 15:
            return self.move2()
        else:
            return self.move1()
